package jp.eisbahn.mgapi.http;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.servlet.http.HttpServletResponse;

import jp.eisbahn.mgapi.util.IOUtils;

import org.json.simple.JSONValue;

/**
 * This class has an basic ability for a HTTP communication.
 * @author Yoichiro Tanaka
 */
public class BasicHttpFetcher implements HttpFetcher {

    /** The Content-Type header name. */
    private static final String CONTENT_TYPE_HEADER_NAME = "Content-Type";

    /*
     * (non-Javadoc)
     * @see jp.eisbahn.mgapi.http.HttpFetcher#fetch(
     * jp.eisbahn.mgapi.http.HttpRequest)
     */
    @Override
    public final HttpResponse fetch(final HttpRequest request)
            throws IOException {
        InputStream requestIn = null;
        InputStreamReader responseIn = null;
        try {
            if (request.isDoOuput()) {
                Map<String, Object> parameterMap = request.getParameterMap();
                ContentType contentType = request.getContentType();
                if (contentType.equals(ContentType.X_WWW_FORM_URLENCODED)) {
                    String body = createFormParameterString(parameterMap);
                    requestIn =
                        new ByteArrayInputStream(body.getBytes("UTF-8"));
                } else if (contentType.equals(ContentType.JSON)) {
                    String body = createParameterJsonString(parameterMap);
                    requestIn =
                        new ByteArrayInputStream(body.getBytes("UTF-8"));
                } else if (contentType.equals(ContentType.BINARY)) {
                    requestIn = request.getBodyStream();
                } else {
                    throw new IllegalArgumentException(
                            "Invalid contentType value. " + contentType);
                }
            }
            String urlString = request.getUrl();
            URL url = new URL(urlString);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod(request.getMethod().toString());
            Map<String, String> headerMap = request.getHeaderMap();
            if (headerMap != null) {
                setContentTypeHeader(headerMap, request.getContentType());
                for (Map.Entry<String, String> entry : headerMap.entrySet()) {
                    conn.setRequestProperty(entry.getKey(), entry.getValue());
                }
            }
            conn.setDoOutput(request.isDoOuput());
            conn.connect();
            if (request.isDoOuput()) {
                OutputStream requestOut = conn.getOutputStream();
                IOUtils.copy(requestIn, conn.getOutputStream());
                requestOut.flush();
            }
            int responseCode = conn.getResponseCode();
            String responseMessage = conn.getResponseMessage();
            String responseBody = "";
            if (responseCode == HttpServletResponse.SC_OK
                    || responseCode == HttpServletResponse.SC_CREATED) {
                responseIn =
                    new InputStreamReader(conn.getInputStream(), "UTF-8");
            } else {
                InputStream errorStream = conn.getErrorStream();
                if (errorStream != null) {
                    responseIn =
                        new InputStreamReader(conn.getErrorStream(), "UTF-8");
                }
            }
            if (responseIn != null) {
                BufferedReader br = new BufferedReader(responseIn);
                String line;
                boolean first = true;
                while ((line = br.readLine()) != null) {
                    if (first) {
                        first = false;
                    } else {
                        responseBody += "\n";
                    }
                    responseBody += line;
                }
            }
            Map<String, List<String>> responseHeaderMap =
                conn.getHeaderFields();
            HttpResponse httpResponse = new BasicHttpResponse(
                    responseCode, responseMessage,
                    responseBody, responseHeaderMap);
            return httpResponse;
        } catch (MalformedURLException e) {
            throw new IllegalStateException(e);
        } catch (ProtocolException e) {
            throw new IllegalStateException(e);
        } finally {
            IOUtils.closeQuietly(requestIn);
            IOUtils.closeQuietly(responseIn);
        }
    }

    /**
     * Set a content type request header.
     * @param headerMap The map object to have request headers.
     * @param contentType The specified content type.
     */
    private void setContentTypeHeader(final Map<String, String> headerMap,
            final ContentType contentType) {
        String header = headerMap.get(CONTENT_TYPE_HEADER_NAME);
        if (header == null) {
            if (contentType.equals(ContentType.X_WWW_FORM_URLENCODED)) {
                headerMap.put(CONTENT_TYPE_HEADER_NAME,
                        "application/x-www-form-urlencoded; charset=UTF-8");
            } else if (contentType.equals(ContentType.JSON)) {
                headerMap.put(CONTENT_TYPE_HEADER_NAME,
                "application/json; charset=UTF-8");
            } else if (contentType.equals(ContentType.BINARY)) {
                throw new IllegalArgumentException(
                        "Content-Type header not specified.");
            }
        }
    }

    /**
     * Create the JSON string from the parameter map.
     * @param parameterMap The map object which has each parameters.
     * @return The JSON string.
     */
    private String createParameterJsonString(
            final Map<String, Object> parameterMap) {
        String result = JSONValue.toJSONString(parameterMap);
        return result;
    }

    /**
     * Create the form string.
     * @param parameterMap The map object which has each parameters.
     * @return The form string.
     */
    private String createFormParameterString(
            final Map<String, Object> parameterMap) {
        try {
            StringBuilder sb = new StringBuilder();
            Set<Entry<String, Object>> entrySet = parameterMap.entrySet();
            boolean first = true;
            for (Entry<String, Object> entry : entrySet) {
                if (first) {
                    first = false;
                } else {
                    sb.append("&");
                }
                sb.append(entry.getKey());
                sb.append("=");
                sb.append(URLEncoder.encode(
                        entry.getValue().toString(), "UTF-8"));
            }
            return sb.toString();
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException(e);
        }
    }

}
